/*
 RNAvigator is a tool for visualizing secondary structure of RNA
 Copyright 2012 Elias La Cour & Peter Bertelsen

 This file is part of RNAvigator.

 RNAvigator is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 RNAvigator is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with RNAvigator.  If not, see <http://www.gnu.org/licenses/>.
 */
package rnavigator.view;

import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.LinkedList;
import rnavigator.model.Base;
import rnavigator.model.Knot;
import rnavigator.model.Structure;

/**
 * @author Elias la Cour & Peter Bertelsen
 */
public class StructureCanvas extends Canvas {

    private boolean debug = false;
    private Structure structure = null;
    private int xCenter, yCenter;
    double radius;
    GUI gui;
    double scale;
    //Graphics2D g2d;
    boolean first = true, firstRunLinear = true;
    AffineTransform tx = new AffineTransform();
    FontMetrics fm;
    Color backgroundColor;
    BufferStrategy buffer;
    private byte view = 0;
    Dimension dimension;
    double[] shapeReactivity;
    double linearLength;
    double circularLength;

    StructureCanvas(GUI gui, Dimension dimension) {
        this.gui = gui;
        this.dimension = dimension;
        scale = 1;
        backgroundColor = Color.WHITE;
        this.setBackground(backgroundColor);
    }

    @Override
    public void paint(Graphics g) {
        if (first) {
            if (this.getBufferStrategy() == null) {
                this.createBufferStrategy(2);
            }
            fm = this.getFontMetrics(getFont());
            xCenter = this.getWidth() / 2;
            yCenter = this.getHeight() / 2;
            tx.translate(xCenter, yCenter);
            radius = structure.getBaseSequence().length * 1.9;
            circularLength = radius * 2;
            System.out.println("Radius set " + radius);
            // sets affinetransform so circular feynman will fit the screen
            setScreenToCircularFeynman();
            first = false;
        }

        buffer = this.getBufferStrategy();
        g = buffer.getDrawGraphics();
        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        g2d.setColor(this.getBackground());
        g2d.fillRect(0, 0, getWidth(), getHeight());
        g2d.setTransform(tx);

        //Linear feynman
        if (getView() == 1 && structure != null) {
            drawLinearFeynman(g2d);
        }

        //Circular feynman
        if (getView() == 2 && structure != null) {
            drawCircularFeynman(g2d);

            if (debug) {
                System.out.println("Structure loaded in paintComponent()...");
                System.out.println("w " + getWidth() + " h " + getHeight());
                System.out.println("guiw " + gui.getWidth() + " guih " + gui.getHeight());
            }
        }
        //Statistics
        if (getView() == 3 && structure != null) {
        }
        g2d.dispose();
        g.dispose();
        buffer.show();
    }

    /**
     * @param structure the structure to set
     */
    public void setStructure(Structure structure) {
        if (debug) {
            System.out.println("structure set in Structurepanel()");
        }
        this.structure = structure;
        repaint();
    }

    public Structure getStructure() {
        return structure;
    }

    public void drawLinearFeynman(Graphics2D g2d) {
        Base b;
        int mate, fontheight;
        double width, height, nucleotideMiddle, distanceBetweenNucleotides, arcX, arcY;
        int nucleotideX = 0, nucleotideY = 0, lineX, lineY1, lineY2, posX, posY, widthP;
        distanceBetweenNucleotides = 9;
        ArrayList<Base> bases = structure.getBases();
        //total length of nucleotide seq. including spaces
        linearLength = structure.getBaseSequence().length * distanceBetweenNucleotides;

        /**
         * increased font height to allow space between nucleotides and arcs
         * actual font height = 16
         */
        fontheight = 22;
        g2d.setColor(Color.BLACK);

        if (debug) {
            System.out.println("dist " + distanceBetweenNucleotides + " getwidth " + getWidth() + " strleght " + structure.getBaseSequence().length);
        }
        //this forloop draws bases and arcs 
        if (structure.getShapeReactivity() == null) {
            for (int i = 0; i < bases.size(); i++) {

                g2d.setColor(Color.black);
                b = bases.get(i);
                mate = b.getMate();
                nucleotideMiddle = fm.stringWidth(b.getNucleotide()) / 2;

                g2d.drawString(b.getNucleotide(), (int) nucleotideX, (int) nucleotideY);

                //for drawing position of nucleotide in the chain, only pos of every 10th nuc is drawn
                if (b.willDrawPos()) {
                    widthP = fm.stringWidth("" + b.getPosition());
                    System.out.println("widthP" + widthP);

                    lineX = nucleotideX + (int) nucleotideMiddle;
                    lineY1 = nucleotideY + 3;
                    lineY2 = nucleotideY + 13;

                    posX = 1 + nucleotideX - (widthP / 4);
                    posY = nucleotideY + 25;
                    g2d.drawLine(lineX, lineY1, lineX, lineY2);
                    g2d.drawString("" + b.getPosition(), posX, posY);
                }

                // to make sure mate is not empty and arcs are not drawn twice
                if (mate != -1 && i < mate) {
                    if (b.isPseudoKnot()) {
                        g2d.setColor(Color.red);
                    } else {
                        g2d.setColor(Color.blue);
                    }
                    width = (mate - b.getPosition()) * distanceBetweenNucleotides;
                    //height - change to set a userdefined height later
                    height = width;
                    //width of nucleotides: A = 7, C = 9, G = 9, U = 9 
                    arcX = (double) nucleotideX + nucleotideMiddle;
                    //NB y point in drawArc() defines TOP of arc
                    arcY = (nucleotideY - height - fontheight) / 2;
                    g2d.drawArc((int) arcX, (int) arcY, (int) width, (int) height, 0, 180);
                }
                nucleotideX += distanceBetweenNucleotides;
            }
        } /**
         * loop specifically for drawing shape reactivity colored diagrams
         */
        else {
            shapeReactivity = structure.getShapeReactivity();
            for (int i = 0; i < bases.size(); i++) {
                b = bases.get(i);
                try {
                    g2d.setColor(getColor(shapeReactivity[i]));

                } catch (IllegalArgumentException e) {
                    System.out.println("Illegal argument parsed as a reactivity double");
                }
                mate = b.getMate();
                nucleotideMiddle = fm.stringWidth(b.getNucleotide()) / 2;

                g2d.drawString(b.getNucleotide(), (int) nucleotideX, (int) nucleotideY);
                //for drawing position of nucleotide in the chain, only pos of every 10th nuc is drawn
                if (b.willDrawPos()) {
                    Color tmpColor = g2d.getColor();
                    g2d.setColor(Color.BLACK);
                    widthP = fm.stringWidth("" + b.getPosition());
                    System.out.println("widthP" + widthP);

                    lineX = nucleotideX + (int) nucleotideMiddle;
                    lineY1 = nucleotideY + 3;
                    lineY2 = nucleotideY + 13;

                    posX = 1 + nucleotideX - (widthP / 4);
                    posY = nucleotideY + 25;
                    g2d.drawLine(lineX, lineY1, lineX, lineY2);
                    g2d.drawString("" + b.getPosition(), posX, posY);
                    g2d.setColor(tmpColor);
                }
                // to make sure mate is not empty and arcs are not drawn twice
                if (mate != -1 && i < mate) {
                    try {
                        g2d.setColor(getColor(shapeReactivity[i]));

                    } catch (IllegalArgumentException e) {
                        System.out.println("Illegal argument parsed as a reactivity double");
                    }
                    width = (mate - b.getPosition()) * distanceBetweenNucleotides;
                    //height - change to set a userdefined height later
                    height = width;
                    //width of nucleotides: A = 7, C = 9, G = 9, U = 9 
                    nucleotideMiddle = fm.stringWidth(b.getNucleotide()) / 2;
                    arcX = (double) nucleotideX + nucleotideMiddle;
                    //NB y point in drawArc() defines TOP of arc
                    arcY = (nucleotideY - height - fontheight) / 2;
                    g2d.drawArc((int) arcX, (int) arcY, (int) width, (int) height, 0, 180);
                }
                nucleotideX += distanceBetweenNucleotides;
            }
        }
        if (firstRunLinear) {
            resetScreen();
            firstRunLinear = false;
        }
    }

    public void drawCircularFeynman(Graphics2D g2d) {
        Base b, b1;
        Arc2D arc;
        Shape nucleotideCircle;
        LinkedList<Knot> knots;
        ArrayList<Base> bases;
        float nucPosX, nucPosY, posLineX1, posLineX2, posLineY1, posLineY2, posX, posY, posWidth, nucWidth, nucMidX, nucMidY;
        int nucleotideX = 0, nucleotideY = 0, widthP;
        //fontHeight = 16
        int fontHeight = fm.getHeight();
        if (debug) {
            System.out.println("drawCircularFeynmanFromUnitMatrix called...");
        }

        bases = structure.getBases();
        knots = structure.getKnots();

        //loop to draw bases
        if (structure.getShapeReactivity() == null) {
            for (int i = 0; i < bases.size(); i++) {
                b = bases.get(i);
                nucWidth = fm.stringWidth(b.getNucleotide());
                g2d.setColor(Color.BLACK);
                nucPosX = (float) ((b.getUnitCircleX() * (radius + 6)) - 4.5);
                nucPosY = (float) ((b.getUnitCircleY() * (radius + 6)) + 4.5);
                nucMidX = (float) ((b.getUnitCircleX() * (radius - 8)) - 4.5);
                nucMidY = (float) ((b.getUnitCircleY() * (radius + 8)) + 4.5);
                //nucleotideCircle = new Ellipse2D.Float((nucPosX), (nucPosY),fontHeight , fontHeight);
                g2d.drawString(b.getNucleotide(), nucPosX, nucPosY);
                g2d.setColor(Color.LIGHT_GRAY);
                //g2d.draw(nucleotideCircle);
                g2d.setColor(Color.BLACK);

                //for drawing position of nucleotide in the chain, only pos. of every 10th nuc. is drawn

                if (b.willDrawPos()) {
                    posLineX1 = (float) ((b.getUnitCircleX() * (radius + 13)));
                    posLineY1 = (float) ((b.getUnitCircleY() * (radius + 13)));
                    posLineX2 = (float) ((b.getUnitCircleX() * (radius + 26)));
                    posLineY2 = (float) ((b.getUnitCircleY() * (radius + 26)));
                    g2d.drawLine((int) posLineX1, (int) posLineY1, (int) posLineX2, (int) posLineY2);
                    posWidth = fm.stringWidth("" + b.getPosition());
                    posX = (float) ((b.getUnitCircleX() * (radius + 30 + (posWidth / 2)))) - (posWidth / 2);
                    posY = (float) ((b.getUnitCircleY() * (radius + 30 + 4))) + 4;
                    g2d.drawString("" + b.getPosition(), posX, posY);
                }

                //g2d.drawString(b.getNucleotide(), (float) ((b.getOffsetX() * (radius))), (float) ((b.getOffsetY() * (radius))));
                //g2d.drawString(b.getNucleotide(), (float) ((b.getOffsetX() * (radius + 6)) - 4.5), (float) ((b.getOffsetY() * (radius + 6)) + 4.5));
            }
            //loop to draw knots
            for (Knot k : knots) {
                b1 = bases.get(k.getPos1() - 1);
                arc = k.getUCArc().getArc(radius);
                if (b1.isKnot()) {
                    g2d.setColor(Color.BLUE);
                }
                if (b1.isPseudoKnot()) {
                    g2d.setColor(Color.RED);
                }
                g2d.draw(arc);
            }
        } //loop to draw bases with SHAPE reactivity
        else {
            shapeReactivity = structure.getShapeReactivity();
            for (int i = 0; i < bases.size(); i++) {
                //for (Base b : bases) {
                b = bases.get(i);

                //HAS TO BE CORRECTED TO ENSURE NO OFF BY ONE ERRORS
                try {
                    g2d.setColor(getColor(shapeReactivity[i]));

                } catch (IllegalArgumentException e) {
                    System.out.println("Illegal argument parsed as a reactivity double");
                }
                //g2d.setColor(Color.BLACK);
                nucPosX = (float) ((b.getUnitCircleX() * (radius + 6)) - 4.5);
                nucPosY = (float) ((b.getUnitCircleY() * (radius + 6)) + 4.5);
                g2d.drawString(b.getNucleotide(), nucPosX, nucPosY);

                if (b.willDrawPos()) {
                    Color tmpColor = g2d.getColor();
                    g2d.setColor(tmpColor);
                    g2d.setColor(Color.BLACK);
                    posLineX1 = (float) ((b.getUnitCircleX() * (radius + 13)));
                    posLineY1 = (float) ((b.getUnitCircleY() * (radius + 13)));
                    posLineX2 = (float) ((b.getUnitCircleX() * (radius + 26)));
                    posLineY2 = (float) ((b.getUnitCircleY() * (radius + 26)));
                    g2d.drawLine((int) posLineX1, (int) posLineY1, (int) posLineX2, (int) posLineY2);
                    posWidth = fm.stringWidth("" + b.getPosition());
                    posX = (float) ((b.getUnitCircleX() * (radius + 30 + (posWidth / 2)))) - (posWidth / 2);
                    posY = (float) ((b.getUnitCircleY() * (radius + 30 + 4))) + 4;
                    g2d.drawString("" + b.getPosition(), posX, posY);
                }
            }
            for (Knot k : knots) {
                b1 = bases.get(k.getPos1() - 1);
                arc = k.getUCArc().getArc(radius);
                try {
                    g2d.setColor(getColor(shapeReactivity[b1.getPosition() - 1]));

                } catch (IllegalArgumentException e) {
                    System.out.println("Illegal argument parsed as a reactivity double");
                }
                g2d.draw(arc);
            }
        }
    }

    public void mouseZoom(MouseWheelEvent evt) {
        Point2D inverse_point;
        Point current_point = evt.getPoint();
        //notches: amount zoomed. notches < 0 = zoomOut, notches >0 = zoomIn
        int notches = evt.getWheelRotation();
        try {
            inverse_point = tx.inverseTransform(current_point, null);
        } catch (NoninvertibleTransformException e) {
            System.err.println("NoninvertibleException");
            return;
        }

        scale *= Math.pow(0.9, notches);
        scale = Math.max(scale, 0.001);

        // Note order of translate - scale - translate is important
        tx.setToIdentity();
        tx.translate(current_point.getX(), current_point.getY());
        tx.scale(scale, scale);
        tx.translate(-inverse_point.getX(), -inverse_point.getY());
        repaint();
    }
    boolean inDrag = false;
    Point previous_point;

    public void mousePressed(MouseEvent evt) {
        previous_point = evt.getPoint();
        inDrag = true;
    }

    public void mouseDragged(MouseEvent evt) {
        Point current_point = evt.getPoint();

        if (inDrag) {
            double x = (current_point.x - previous_point.x) / scale;
            double y = (current_point.y - previous_point.y) / scale;
            if (debug) {
                System.out.println("(x,y) " + "(" + x + "," + y + ")");
            }
            previous_point = current_point;
            tx.translate(x, y);
            repaint();
        }
    }

    public void mouseReleased(MouseEvent evt) {
        inDrag = false;
    }

    /**
     *
     * @param originx y coordinate to define new origin, this will be the new
     * (0,0)
     * @param originy y coordinate to define new origin, this will be the new
     * (0,0)
     * @param scale
     */
//    public void resetTransform(int originx, int originy, double scale) {
//        tx.setToIdentity();
//        tx.translate(originx, originy);
//        tx.scale(scale, scale);
//        repaint();
//    }
    /**
     * @return the view
     */
    public byte getView() {
        return view;
    }

    /**
     * @param view the view to set
     */
    public void setView(byte view) {
        this.view = view;
    }

    public void resetScreen() {
        if (view == 1) {
            setScreenToLinearFeynman();
        }
        if (view == 2) {
            setScreenToCircularFeynman();
        } else {
            System.out.println("resetScreen(): view not recognized");
        }
    }

    /**
     * Scales down diagram to fit screen
     */
    public void setScreenToLinearFeynman() {
        double screenWidth = this.getWidth() - 100;
        scale = (screenWidth) / linearLength;
        System.out.println("setScaleToScreen " + scale);
        tx.setToIdentity();
        tx.translate(25, getHeight() - 50);
        tx.scale(scale, scale);
        repaint();
    }

    /**
     * Scales down diagram to fit screen
     */
    public void setScreenToCircularFeynman() {
        double screenSize = (Math.min(this.getWidth(), this.getHeight())) - 100;
        scale = screenSize / circularLength;
        System.out.println("setScaleToScreen " + scale);
        tx.setToIdentity();
        tx.translate(xCenter, yCenter);
        tx.scale(scale, scale);
        repaint();
    }

    public Color getColor(double reactivity) throws IllegalArgumentException {
        if (reactivity < 0) {
            return Color.BLACK;
            //throw new IllegalArgumentException();
        }
        if (reactivity < 0.2) {
            return Color.BLACK;
        }
        if (reactivity < 0.5) {
            return Color.GREEN;
        }
        if (reactivity < 0.8) {
            return Color.ORANGE;
        }
        if (reactivity < 1.0) {
            return Color.RED;
        } else {
            return Color.BLACK;
            //throw new IllegalArgumentException();
        }
    }

    public BufferedImage getDiagram() {
        int width = this.getWidth();
        int height = this.getHeight();
        BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = bi.createGraphics();
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        g2d.setBackground(Color.white);
        g2d.clearRect(0, 0, width, height);
        g2d.setTransform(tx);

        //Linear feynman
        if (getView() == 1 && structure != null) {
            drawLinearFeynman(g2d);
        }

        //Circular feynman
        if (getView() == 2 && structure != null) {
            drawCircularFeynman(g2d);

            if (debug) {
                System.out.println("Structure loaded in paintComponent()...");
                System.out.println("w " + getWidth() + " h " + getHeight());
                System.out.println("guiw " + gui.getWidth() + " guih " + gui.getHeight());
            }
        }
        g2d.dispose();
        return bi;
    }
}
